
/*
 * Copyright (C) 2008-2009 Archie L. Cobbs. All rights reserved.
 *
 * $Id: Entity.java 377 2010-03-11 20:50:18Z archie.cobbs $
 */

package org.dellroad.sidekar;

import java.lang.reflect.Constructor;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.dellroad.sidekar.query.QueryIterable;
import org.dellroad.sidekar.query.QueryIterator;
import org.dellroad.sidekar.query.QuerySet;
import org.dellroad.sidekar.util.SidekarException;

/**
 * Sidekar entity data structure. An entity contains a set of {@link Field}s.
 * Instances of an entity are represented in Java by {@link Item}s.
 *
 * <p>
 * Instances of this class may only be used for database access while the containing
 * {@link Database} remains open. If a database is closed and then re-opened,
 * a new {@link Entity} instance must be acquired (e.g., from {@link Database#getEntity}).
 * </p>
 *
 * @param <T> associated user model class
 * @see org.dellroad.sidekar.annotation.SidekarEntity
 */
public final class Entity<T> extends SchemaElement implements QueryIterable<T> {

    /**
     * By convention, this is the Sidekar ID of a singleton item.
     */
    public static final long SINGLETON_ID = 1;

    private boolean isAbstract;                         // true if entity is abstract
    private final Entity<? super T> parent;             // parent entity, or null if we are the top entity in hierarchy
    private final Class<T> userClass;                   // user-supplied model class
    private final Class<? extends T> modelClass;        // this entity's generated model class (subclass of userClass)
    private Map<String, Field<?>> entityFieldMap        // all fields in this entity indexed by name
      = new HashMap<String, Field<?>>();
    private final HashMap<String, Index<T>> indexMap    // indexes on this entity
      = new HashMap<String, Index<T>>();

    private ItemCache<T> itemCache;                     // cache for this hierarchy's model objects
    private Entity<? super T> topParent;                // the top entity in my hierarchy
    private Map<String, Field<?>> fieldNameMap;         // all fields in this entity indexed by name
    private Constructor<? extends T> constructor;       // constructor for model class (null if abstract)

    /**
     * Constructor.
     *
     * @param db associated database
     * @param name name by which this entity is known
     * @param isAbstract whether entity is abstract
     * @param parent entity which this one extends, if any, else null
     * @param userClass user-supplied model class associated with this instance
     * @param modelClass generated model class for Java {@link Item} representations
     */
    @SuppressWarnings("unchecked")
    Entity(Schema schema, String name, boolean isAbstract, Entity<?> parent,
      Class<T> userClass, Class<? extends T> modelClass) {
        super(schema, name);
        Sidekar.checkValidEntityName(name);
        this.isAbstract = isAbstract;
        this.parent = (Entity<Object>)parent;
        assert userClass != null;
        this.userClass = userClass;
        assert modelClass != null;
        this.modelClass = modelClass;
    }

// Entity operations

    /**
     * Get the parent of this entity, or null if none.
     */
    public Entity<? super T> getParent() {
        return this.parent;
    }

    /**
     * Get the top level entity in my hierarchy.
     */
    public Entity<? super T> getTopEntity() {
        return this.topParent;
    }

    /**
     * Is this entity the top level entity in its hierarchy?
     */
    public boolean isTopEntity() {
        return this.parent == null;
    }

    /**
     * Cast the given object to an instance of this entity.
     *
     * @throws ClassCastException if the given object is not an {@link Item} instance of this entity
     * @throws IllegalStateException if the database containing this entity has been closed
     */
    public T cast(Object obj) {
        this.validate();
        if (obj == null)
            return null;
        Item<?> item = (Item<?>)obj;
        if (this.isAncestorOf(item.getSidekarEntity()))
            throw new ClassCastException("not an instance of " + this);
        return this.userClass.cast(item);
    }

    /**
     * Determine if an item is an instance of this entity (or a sub-entity).
     *
     * @throws IllegalStateException if the database containing this entity has been closed
     */
    public boolean isInstance(Object obj) {
        this.validate();
        if (!(obj instanceof Item))
            return false;
        Item<?> item = (Item<?>)obj;
        return this.isAncestorOf(item.getSidekarEntity());
    }

    /**
     * Determine if the given entity is equal to, or a sub-entity of, this entity.
     */
    public boolean isAncestorOf(Entity<?> entity) {
        this.validate();
        if (!this.inSameHierarchy(entity))
            return false;
        do {
            if (entity == this)
                return true;
            entity = entity.parent;
        } while (entity != null);
        return false;
    }

    /**
     * Cast an entity as a sub-entity of this instance.
     *
     * @throws IllegalArgumentException if {@code entity} is not this entity or a sub-entity
     */
    @SuppressWarnings("unchecked")
    public Entity<? extends T> asSubEntity(Entity<?> entity) {
        if (!this.isAncestorOf(entity))
            throw new IllegalArgumentException(entity + " is not a sub-entity of " + this);
        return (Entity<? extends T>)entity;
    }

    /**
     * Determine if this entity is abstract.
     */
    public boolean isAbstract() {
        return this.isAbstract;
    }

    void setAbstract(boolean isAbstract) {
        this.isAbstract = isAbstract;
    }

    /**
     * Determine if this and the given entity are in the same hierarchy.
     * That is, determine if this and the given entity have a common ancestor entity.
     */
    public boolean inSameHierarchy(Entity<?> entity) {
        return this.topParent == entity.topParent;
    }

    @Override
    public String toString() {
        return "entity " + this.getName();
    }

// Field introspection

    /**
     * Get all of the fields in this entity.
     *
     * <p>
     * {@linkplain ImplicitField Implicit fields} and ancestory entity fields are included.
     * </p>
     *
     * @return all fields defined for this entity, including ancestor and implicit fields
     */
    public Set<Field<?>> getFields() {
        this.validate();
        return Collections.unmodifiableSet(new HashSet<Field<?>>(this.fieldNameMap.values()));
    }

    /**
     * Retrieve a field in this entity by name.
     *
     * <p>
     * Ancestor fields and {@link ImplicitField implicit fields} may be accessed using this method.
     * </p>
     *
     * @param name field name (possibly an implicit field name)
     * @return the field with that name
     * @throws IllegalArgumentException if no such field exists
     * @see #getFieldMap
     */
    public Field<?> getField(String name) {
        this.validate();
        Field<?> field = this.fieldNameMap.get(name);
        if (field == null)
            throw new IllegalArgumentException("field \"" + name + "\" not found in " + this);
        return field;
    }

    /**
     * Get a mapping of all fields in this entity indexed by name.
     *
     * <p>
     * The returned mapping includes ancestor entity fields
     * as well as all of the {@link ImplicitField implicit fields}.
     * </p>
     *
     * @return an unmodifiable mapping from field name to field
     * @see #getField(String)
     */
    public Map<String, Field<?>> getFieldMap() {
        return Collections.unmodifiableMap(this.fieldNameMap);
    }

// Index introspection

    /**
     * Get all indexes associated with this entity.
     *
     * @return all indexes inside an unmodifiable set
     * @see #getIndexMap
     */
    public Set<Index<T>> getIndexes() {
        this.validate();
        return Collections.unmodifiableSet(new HashSet<Index<T>>(this.indexMap.values()));
    }

    /**
     * Get the index having the given name.
     *
     * @param name name of an index associated with this entity
     * @throws IllegalArgumentException if no such index is associated with this entity
     */
    public Index<T> getIndex(String name) {
        this.validate();
        Index<T> index = this.indexMap.get(name);
        if (index == null)
            throw new IllegalArgumentException("no index named \"" + name + "\" is associated with " + this);
        return index;
    }

    /**
     * Get the {@link Index}s on this entity.
     *
     * @return an unmodifiable mapping from {@link Index} name to {@link Index}
     */
    public Map<String, Index<T>> getIndexMap() {
        return Collections.unmodifiableMap(this.indexMap);
    }

// Index operations

    /**
     * Convenience method finding items using an index. Equivalent to:
     *
     *  <blockquote>
     *  {@code getIndex(name).get(values)}
     *  </blockquote>
     *
     * @throws IllegalStateException if the database containing this entity has been closed
     */
    public QuerySet<T> find(String indexName, Object... values) {
        return this.getIndex(indexName).query(values);
    }

    /**
     * Convenience method finding one (the first) item using an index. Equivalent to:
     *
     *  <blockquote>
     *  {@code getIndex(name).getFirst(values)}
     *  </blockquote>
     *
     * @throws IllegalStateException if the database containing this entity has been closed
     */
    public T findUnique(String indexName, Object... values) {
        return this.getIndex(indexName).getFirst(values);
    }

// Item operations

    /**
     * Create a new attached instance of this entity.
     *
     * <p>
     * The item's fields will have their default values.
     * </p>
     *
     * <p>
     * The item will remain in the database until it is detached.
     * </p>
     *
     * <p>
     * Newly created {@link Item}s are guaranteed to have {@link Item#getSidekarId ID's} which
     * have never been assigned to any other instance in the underlying datastore.
     * </p>
     *
     * @throws IllegalArgumentException if this entity is abstract
     * @throws IllegalStateException if the database containing this entity has been closed
     * @throws SidekarException if there is an underlying database exception
     * @see #createDetached
     * @see Database#create
     */
    public T create() {

        // Check open
        this.validate();

        // Check abstractness
        if (this.isAbstract())
            throw new IllegalArgumentException(this + " is an abstract entity");

        // Create a new item in the datastore
        long id = this.getDatastore().instanceCreate(this);
        assert id != 0;

        // Add it to the item cache and return it
        return this.itemCache.get(id);
    }

    /**
     * Get the one instance of this entity, which is assumed to be a singleton entity.
     *
     * <p>
     * This method returns the item with ID equal to {@link #SINGLETON_ID}, automatically creating and attaching it if necessary.
     *
     * @throws IllegalStateException if the database containing this entity has been closed
     */
    public T getSingleton() {
        T item = get(SINGLETON_ID);
        ((Item<?>)item).sidekarAttach();
        return item;
    }

    /**
     * Attach item.
     *
     * <p>
     * Normally {@link Item#sidekarAttach} should be used instead of this method.
     *
     * @throws IllegalArgumentException if item is not of this entity's exact type
     * @throws IllegalStateException if the database containing this entity has been closed
     */
    public void attach(Item<T> item) {

        // Check database
        this.validate();

        // Check entity
        if (item.getSidekarEntity() != this)
            throw new IllegalArgumentException("item is not an instance of " + this);

        // Attach item
        long id = item.getSidekarId();
        assert id != 0;
        this.getDatastore().instanceAttach(this, id);
    }

    /**
     * Detach item.
     *
     * <p>
     * Normally {@link Item#sidekarDetach} should be used instead of this method.
     *
     * @throws IllegalArgumentException if item is not of this entity's exact type
     * @throws IllegalStateException if the database containing this entity has been closed
     */
    public void detach(Item<T> item) {

        // Check database
        this.validate();

        // Check entity
        if (item.getSidekarEntity() != this)
            throw new IllegalArgumentException("item is not an instance of " + this);

        // Detach item
        long id = item.getSidekarId();
        assert id != 0;
        this.getDatastore().instanceDetach(this, id);
    }

    /**
     * Determine if item is attached.
     *
     * <p>
     * Normally {@link Item#isSidekarAttached} should be used instead of this method.
     *
     * @throws IllegalArgumentException if item is not of this entity's exact type
     * @throws IllegalStateException if the database containing this entity has been closed
     */
    public boolean isAttached(Item<T> item) {

        // Check database
        this.validate();

        // Check entity
        if (item.getSidekarEntity() != this)
            throw new IllegalArgumentException("item is not an instance of " + this);

        // Check status
        long id = item.getSidekarId();
        assert id != 0;
        return this.getDatastore().instanceIsAttached(this, id);
    }

    /**
     * Validate item.
     *
     * <p>
     * Normally {@link Item#sidekarValidate} should be used instead of this method.
     *
     * @throws IllegalStateException if the database containing this entity has been closed
     */
    public Item<T> validate(Item<T> item) {
        if (!this.isAttached(item))
            throw new DetachedItemException(item);
        return item;
    }

    /**
     * Get an {@link Item} instance of this entity by its unique ID.
     *
     * <p>
     * This will return the Java object that represents the item with the given id.
     * If no item with the given id exists in the database, a non-null {@link Item} is
     * still returned, but it will be detached, i.e., all fields will be inaccessible.
     * </p>
     *
     * <p>
     * Normally this method should only be used for debugging, and only when it is
     * known that the {@link Item} already exists (or was created previously).
     * </p>
     *
     * @param id item's id; must be positive
     * @return canonical {@link Item} instance with the given id
     * @throws IllegalArgumentException if this entity is abstract
     * @throws IllegalArgumentException if the id is invalid
     * @throws IllegalStateException if the database containing this entity has been closed
     * @see #create
     * @see #createDetached
     * @see Item#getSidekarId
     */
    public T get(long id) {
        this.validate();
        if (this.isAbstract())
            throw new IllegalArgumentException(this + " is an abstract entity");
        T item = this.itemCache.get(id);
        assert ((Item<?>)item).getSidekarEntity() == this;
        return item;
    }

// Query operations

    @Override
    public boolean isReadOnly() {
        return false;
    }

    /**
     * Iterate over all instances of this entity. This method is equivalent to:
     * <blockquote><code>
     * query().iterator();
     * </blockquote></code> 
     */
    @Override
    public QueryIterator<T> iterator() {
        return this.query().iterator();
    }

    /**
     * Get all items having type (or sub-type) of this entity.
     *
     * <p>
     * Collections views support instance removal. If a remove method is invoked, the most recently returned
     * {@link Item} is detached (as if by {@link Item#sidekarDetach Item.sidekarDetach()}).
     * </p>
     *
     * <p>
     * This is a convenience method, equivalent to:
     *  <blockquote><code>
     *  iterator(false)
     *  </code></blockquote>
     * </p>
     *
     * @throws IllegalStateException if the database containing this entity has been closed
     * @see #iterator(boolean)
     */
    public QuerySet<T> query() {
        return this.query(false);
    }

    /**
     * Get all items having type of this entity.
     * The {@code exact} parameter determines whether only items exactly of this entity's
     * type are returned, or whether sub-entity items are also included.
     *
     * <p>
     * When {@code exact} is {@code false}, {@link Item}s of a specific (sub-)entity will always
     * appear together contiguously. The order in which sub-entity groups appear is arbitrary but
     * consistent.
     * </p>
     *
     * <p>
     * For any specific (sub-)entity, that entity's items will appear in order of their
     * {@linkplain Item#getSidekarId id's}.
     * </p>
     *
     * <p>
     * Collections views support instance removal. If a remove method is invoked, the most recently returned
     * {@link Item} is detached (as if by {@link Item#sidekarDetach Item.sidekarDetach()}).
     * </p>
     *
     * @param exact whether to iterate only items with this entity's exact type ({@code true})
     *  or to include sub-entity items as well ({@code false})
     * @throws IllegalStateException if the database containing this entity has been closed
     */
    public QuerySet<T> query(boolean exact) {
        this.validate();
        return this.getDatastore().entityAccess(this, exact);
    }

// Internal API

    /**
     * Create a new instance with the given ID.
     * This method should only be used by {@link ItemCache}.
     */
    T instantiate(long id) {
        try {
            return this.constructor.newInstance(id, this);
        } catch (RuntimeException e) {
            throw e;
        } catch (Exception e) {
            throw new RuntimeException("internal error", e);
        }
    }

    /**
     * Get the user model class.
     */
    Class<T> getUserClass() {
        return this.userClass;
    }

    /**
     * Get the generated model class.
     *
     * <p>
     * This class will be a subclass of the {@link #getUserClass user class} and also implement {@link Item}.
     */
    Class<? extends T> getModelClass() {
        return this.modelClass;
    }

    /**
     * Get the {@link Field}s strictly in this entity.
     */
    Map<String, Field<?>> getEntityFields() {
        return this.entityFieldMap;
    }

    /**
     * Get the {@link Entity} at the top of my hierarchy (possibly me).
     */
    Entity<? super T> getTopParent() {
        return this.topParent;
    }

    /**
     * Read the content of a {@link SimpleField} from the given item.
     * This assumes the item is not null and an instance of this entity.
     *
     * @param item an instance of this {@link Entity}
     * @param field field to read
     * @return input containing encoded field content
     * @throws IllegalArgumentException if this entity does not contain the field
     * @throws DetachedItemException if item does not exist in the database
     * @throws IllegalStateException if the database containing this entity has been closed
     */
    <E> E readField(Item<?> item, SimpleField<E> field) {

        // Sanity check
        assert this.isInstance(item);

        // Read field
        long id = item.getSidekarId();
        assert id != 0;
        return this.getDatastore().simpleFieldRead(this, id, field);
    }

    /**
     * Update a {@link SimpleField} in the given item with new content.
     * This assumes the item is not null and an instance of this entity.
     *
     * @param item an instance of this {@link Entity}
     * @param field field to read
     * @param entry entry containing the data for this new content
     * @throws IllegalArgumentException if this entity does not contain the field
     * @throws DetachedItemException if item does not exist in the database
     * @throws IllegalStateException if the database containing this entity has been closed
     */
    <E> void writeField(Item<?> item, SimpleField<E> field, E value) {

        // Sanity check
        assert isInstance(item);

        // Write field
        long id = item.getSidekarId();
        assert id != 0;
        this.getDatastore().simpleFieldWrite(this, id, field, value);
    }

// Initialization

    /**
     * Generate all derived information. This assumes that this method has already been invoked on the parent.
     */
    void initialize() {

        // Sanity check
        assert this.isTopEntity() || this.parent.topParent != null;
        assert this.itemCache == null;

        // Find the top level entity in my hierarchy
        if (this.isTopEntity())
            this.topParent = this;
        else
            this.topParent = this.parent.topParent;
        assert this.topParent != null;

        // Get item cache
        this.itemCache = new ItemCache<T>(this);

        // Build field by name mapping, checking uniqueness of field names in the process
        HashMap<String, Field<?>> nonImplicitFields = this.buildNonImplicitFields();
        this.fieldNameMap = new HashMap<String, Field<?>>(nonImplicitFields.size() + 3);
        this.fieldNameMap.putAll(nonImplicitFields);
        this.fieldNameMap.put(Sidekar.ID_FIELD_NAME, this.getSchema().getIdField());
        this.fieldNameMap.put(Sidekar.ENTITY_FIELD_NAME, this.getSchema().getEntityField());
        this.fieldNameMap.put(Sidekar.ATTACHED_FIELD_NAME, this.getSchema().getAttachedField());

        // Get constructor
        if (!this.isAbstract()) {
            try {
                this.constructor = this.modelClass.getConstructor(Long.TYPE, Entity.class);
            } catch (Exception e) {
                throw new RuntimeException("internal error", e);
            }
        } else
            this.constructor = null;
    }

    /**
     * Build map of all fields for this entity, including ancestor fields, but not including implicit fields.
     */
    HashMap<String, Field<?>> buildNonImplicitFields() {
        Map<String, Field<?>> parentMap = this.parent != null ?
          this.parent.buildNonImplicitFields() : Collections.<String, Field<?>>emptyMap();
        HashMap<String, Field<?>> fieldMap = new HashMap<String, Field<?>>(parentMap.size() + this.entityFieldMap.size());
        fieldMap.putAll(parentMap);
        fieldMap.putAll(this.entityFieldMap);
        return fieldMap;
    }

// Schema modification support

    void addField(Field<?> field) {
        this.entityFieldMap.put(field.getName(), field);
    }

    void addIndex(String name, List<String> fieldNames, boolean unique) {
        Index<T> index = new Index<T>(this, name, fieldNames, unique);
        this.indexMap.put(name, index);
    }

    void removeIndex(String name) {
        this.indexMap.remove(name);
    }

    void removeField(Field<?> field) {
        this.entityFieldMap.remove(field);
    }
}

